--[[ Copyright 2012, Chris Ho                               ]]--
--[[ MIT License (Please read the LICENSE file for details) ]]--

Use("common/Position");
Use("common/Task");
Use("common/Creature");
Use("common/Pet");
Use("common/Skill");
Use("tasks/Follow");
Use("tasks/Charge");
Use("tasks/Retreat");
Use("tasks/Patrol");

-- The default persona should work for all pet types, but doesn't
-- take into account special abilities/skills. The default
-- Think/Stop/Move/Follow/Attack function are open for change
-- in personality updates.
--
-- Note: This file directly modifies the global ME variable. It
-- redirects the function calls for the standard pet behaviors to
-- the default Persona.
--
-- TODO: For Lua 5.1 or higher:
-- Run the contents of the file as a function, passing in the
-- desired instance to personify. We return a reference to the
-- updated instance.

Persona = {};



--[[ React to the current situation ]]--
--  We may change mode. This function allows the pet to act on its own.
function Persona.React(self)
   -- Fall back to default behavior if we're too far away.
  if (self:Measure() > LEASH) then
    Info("Persona - React() Too far from owner!");
    self:Behave(MODE_DEFAULT);
  end

  Pet.React(self);
end

--[[ Decide what to do based on the situation ]]--
--  We may change mode. This function allows the pet to act on its own.
function Persona.Think(self)
  -- Basic (raw) Modes
  if (self.mode == MODE_STOP) then self:HandleStop()
  elseif (self.mode == MODE_MOVE) then self:HandleMove()
  elseif (self.mode == MODE_FOLLOW) then self:HandleFollow()
  -- Intelligent Modes
  elseif (self.mode == MODE_ESCORT) then self:HandleEscort()
  elseif (self.mode == MODE_PATROL) then self:HandlePatrol()
  elseif (self.mode == MODE_ASSAULT) then self:HandleAssault()
  elseif (self.mode == MODE_DEFEND) then self:HandleDefend()
  elseif (self.mode == MODE_AVOID) then self:HandleAvoid()
  -- Invalid Modes:
  else Alert("Persona - Think() Unknown Mode: " .. self.mode)  end

  -- TODO: overrides
  -- Check owner's health. if it's too low, try to heal.
  -- If the owner is under attack, assist him.
  -- Otherwise, if something is attacking me, fight back.

  -- If I am attacking something, try to use a combat skill.
  -- If I am hurt, try a recovery skill.
end

--[[ Select a good target from a list of possible targets ]]--
-- Returns 0 if no targets are valid.
function Persona.SelectTarget(self, _targets)
  local index = 1;
  local candidate = 0;
  local score = 0;

  -- Avoid Kill-Steal. Find the first valid target.
  while (index <= _targets.n) do
    local foe = Creature.Create(_targets[index]);
    local attackers = foe:Scan(ACTORS);
    local offset = 1;

    -- Avoid kill-stealing.
    if (self:FetchFriend(GetV(V_TARGET, foe.id)) == nil) then
      -- If the foe is not attacking an ally, then we should check the
      -- foe's list of attackers.
      while (offset <= attackers.n) do
        -- Check the attacker against our friends table
        if (self:FetchFriend(attackers[offset]) == nil) then
          -- Someone else is attacking this foe.
          Debug(foe.id .. " is someone else's problem");
          break;
        end

        offset = offset + 1;
      end
    end

    -- If we went through the whole attacker list, then the foe is valid.
    if (offset > attackers.n) then
      local value = self:RateTarget(foe.id);
      if (value > score) then
        -- We found a candidate that's better than the previous candidate.
        Debug(foe.id .. " score " .. value);
        candidate = foe.id;
        score = value;
      end
    end

    index = index + 1;
  end

  -- Return best candidate, or 0 for no good target.
  return candidate;
end



--[[ Nothing is Happening ]]--
function Persona.Idle(self)
end

--[[ The pet should help the given target ]]--
-- By default, we assist its target. We may also use support skills.
function Persona.Help(self, _target)
  local threat = self:SelectTarget(Creature.Create(_target):Scan(ACTORS));

  -- If nobody is attacking this target, then see if this target is
  -- attacking something.
  if (threat == 0) then threat = GetV(V_TARGET, _target) end
  if (threat ~= 0) then
    -- If any threats are valid, we will assist with an attack.
    self:Attack(threat)
  end
end

--[[ The pet wants to change task to a stand ]]--
function Persona.Stop(self)
  -- Reset properties when stoped.
  self.destination = Position.Find(self.id);
  self.target = 0;
  self.leader = self.owner.id;
  self.combo = 0;
  self.task = Task.Create(self);
  Info("Stopping");
end
--[[ The pet wants to change task to a move ]]--
function Persona.Move(self, _destination)
  self.destination = _destination;
  self.task = Go.Create(self, self.destination);
  Debug("Going to " .. _destination.x .. "," .. _destination.y);
end
--[[ The pet wants to change task to a follow ]]--
-- Follow is a repeatable task.
function Persona.Follow(self, _target)
  self.target = _target;
  self.task = Follow.Create(self, self.target);
  Info("Following " .. _target);
end
--[[ The pet wants to change task to a targeted attack ]]--
-- For archers, we should use attack. For all others, we should use charge.
function Persona.Attack(self, _target)
  self.target = _target;
  self.task = Charge.Create(self, Skill.None(self.id), self.target);
  Debug("Attacking " .. self.target .. " with " .. self.task.skill.code .. "#" .. self.task.skill.level);
end
--[[ The pet wants to run away from a position ]]--
function Persona.Retreat(self, _hotspot)
  self.task = Retreat.Create(self, _hotspot);
  Info("Retreating from " .. _hotspot.x .. "," .. _hotspot.y);
end



--[[ Stop and Do Nothing ]]--
function Persona.HandleStop(self)
  Debug("Persona - HandleStop()");

  self:Stop();
end

--[[ Move Only ]]--
function Persona.HandleMove(self)
  Debug("Persona - HandleMove()");

  -- Move to the stored destination.
  local from = Position.Find(self.id);
  local to = self.destination;

  if ((from.x == to.x) and (from.y == to.y)) then
    Debug("Arrived at " .. to.x .. "," .. to.y);
    -- Stay in this mode (so we try to come back if we do other things).
    self:Idle();
  else
    self:Move(to);
  end
end

--[[ Follow Only ]]--
function Persona.HandleFollow(self)
  Debug("Persona - HandleFollow()");

  -- Move to the stored target.
  local from = Position.Find(self.id);
  local to = Position.Find(self.leader);

  if (Position.Measure(from, to) < self:GetLeash()) then
    Debug("Waiting for " .. self.leader);
    -- Stay in this mode and just wait.
    self:Idle();
  else
    self:Follow(self.leader);
  end
end

--[[ Escort (Attack and Follow) ]]--
function Persona.HandleEscort(self)
  Debug("Persona - HandleEscort()");

  -- Find any foes. Break and engage if we find any.
  local from = Position.Find(self.id);
  local to = Position.Find(self.leader);
  local threat;

  -- First check our foes.
  threat = self:SelectTarget(self:Scan(ACTORS));
  if (threat ~= 0) then
    -- Take the first valid threat from the list as our target.
    self:Attack(threat);
    return;
  end

  -- Now check leader's foes.
  threat = self:SelectTarget(Creature.Create(self.leader):Scan(ACTORS));
  if (threat ~= 0) then
    -- The leader has hostile creatures on him.
    self:Attack(threat);
    return;
  end

--[[
  -- If leader is attacking someone and I can't see it,
  -- then try to move a step towards the leader.
  if (GetV(V_TARGET, self.owner.id) ~= 0) then
    local range = Position.Measure(from, to) - 1;
    if (range < 1) then range = 1 end

    local waypoint = from:Route(to, range);
    self:Move(waypoint);
  end
]]--

  -- Now check foes of friends.
  local key, value;
  for key, value in ipairs(self.friends) do
    threat = self:SelectTarget(value:Scan(ACTORS));
    if (threat ~= 0) then
      -- A friend has hostile creatures on him.
      self:Attack(threat);
      return;
    end
  end

  -- No valid targets, so just follow.
  if (Position.Measure(from, to) < self:GetLeash()) then
    Debug("Waiting for " .. self.leader);
    -- Stay in this mode and just wait.
    self:Idle();
  else
    self:Follow(self.leader);
  end
end

--[[ Patrol (Move and Attack Between Destinations) ]]--
function Persona.HandlePatrol(self)
  Debug("Persona - HandlePatrol()");

  -- The patrol task goes back and forth between two positions.
  self.task = Patrol.Create(self, self.destination);
  self.destination = self.task.origin;
end

--[[ Assault (Wander and Fire at Will) ]]--
function Persona.HandleAssault(self)
  Debug("Persona - HandleAssault()");

  -- TODO: UNTESTED

  -- Find best target candidate from list of all creatures.
  local candidate = self:SelectTarget(self:Filter(ACTORS, VISIBILITY, 50 / self:GetTranquility()));
  if (candidate ~= 0) then
    -- We have a potential target. Let's kill it.
    self:Attack(candidate);
    return;
  end

  local from = Position.Find(self.id);
  local to = Position.Find(self.leader);
  if (Position.Measure(from, to) < self:GetLeash()) then
    Debug("Waiting for " .. self.leader);
    -- Nothing to do.
    self:Idle();
  else
    -- Go to the leader if you have no valid targets.
    self:Follow(self.leader);
  end
end

--[[ Defend (Stay and Fire at Will) ]]--
function Persona.HandleDefend(self)
  Debug("Persona - HandleDefend()");

  local from = Position.Find(self.id);
  local to = self.destination;
  local threat;

  -- First check our foes.
  threat = self:SelectTarget(self:Scan(ACTORS));
  if (threat ~= 0) then
    -- Take the first threat from the list as our target.
    self:Attack(threat);
    return;
  end

  -- Now check leader's foes.
  threat = self:SelectTarget(self.owner:Scan(ACTORS));
  if (threat ~= 0) then
    -- The leader has hostile creatures on him.
    self:Attack(threat);
    return;
  end

  -- Now check foes of friends.
  local key, value;
  for key, value in ipairs(self.friends) do
    threat = self:SelectTarget(value:Scan(ACTORS));
    if (threat ~= 0) then
      -- A friend has hostile creatures on him.
      self:Attack(threat);
      return;
    end
  end

  if ((to.x == from.x) and (to.y == from.y)) then
    Debug("Defending " .. to.x .. "," .. to.y);
    self:Idle();
  else
    self:Move(to);
  end
end

--[[ Avoid (Evade and Keep Away from Foes) ]]--
function Persona.HandleAvoid(self)
  Debug("Persona - HandleAvoid()");

  -- TODO: UNTESTED

  local threats = self:Scan(ACTORS);
  local total = 0;
  local here = Position.Find(self.id);
  local middle = Position.Create(here.x, here.y);

  -- Get the three closest threats.
  local index = 1;
  while (total < 3) do
    if (threats[index]) then
      local there = Position.Find(threats[index]);
      if (Position.Measure(here, there) < VISIBILITY) then
        -- Remember: the array is one-based.
        total = total + 1;
        middle.x = middle.x + there.x;
        middle.y = middle.y + there.y;
      end
    else
      break;
    end

    index = index + 1;
  end

  -- If there are threats, run. Otherwise, follow and use recovery skills.
  if (total > 0) then
    -- Now retreat from the center of action.
    self:Retreat(middle);
  else
    -- TODO: use recovery skills
    Debug("Trying to recover...");
    self:Idle();
  end
end



--local instance = ...;
--local instance = arg[1];

-- We apply the persona to the global ME instance.
local instance = ME;

instance.React = Persona.React;
instance.Think = Persona.Think;
instance.SelectTarget = Persona.SelectTarget;
instance.Idle = Persona.Idle;
instance.Help = Persona.Help;
instance.Stop = Persona.Stop;
instance.Move = Persona.Move;
instance.Follow = Persona.Follow;
instance.Attack = Persona.Attack;
instance.Retreat = Persona.Retreat;
instance.HandleStop = Persona.HandleStop;
instance.HandleMove = Persona.HandleMove;
instance.HandleFollow = Persona.HandleFollow;
instance.HandleEscort = Persona.HandleEscort;
instance.HandlePatrol = Persona.HandlePatrol;
instance.HandleAssault = Persona.HandleAssault;
instance.HandleDefend = Persona.HandleDefend;
instance.HandleAvoid = Persona.HandleAvoid;

Info("Main Persona Loaded");
return instance;
